package com.selebash;

import com.selebash.config.Config;
import com.selebash.ast.AbstractAst;
import com.selebash.ast.CallFunctionAst;
import com.selebash.ast.FeatureAst;
import com.selebash.ast.IfAst;
import com.selebash.ast.SequenceAst;
import com.selebash.exception.SelebashParseException;
import com.selebash.features.FeatureHolder;
import java.util.ArrayList;
import java.util.List;
import java.util.regex.Matcher;
import org.apache.commons.lang3.StringUtils;

/**
 * @author sad
 */
public class FunctionParser {

    private final ParserContext context;

    public FunctionParser(ParserContext context) {
        this.context = context;
    }

    public List<AbstractAst> readArrayOfAst(ParserInputStream stream, Config config) {
        int tabLevel = stream.getTabLevel();
        int index = stream.getIndex();
        List<AbstractAst> seqNodes = new ArrayList<>();
        AbstractAst lastnode = null;
        while (true) {
            stream.skipBlank();
            int lineTabLevel = stream.getTabLevel();
            if (lineTabLevel < tabLevel) {
                break;
            }

            if (lineTabLevel > tabLevel) {
                if (lastnode != null && lastnode.getFeatureHolder() != null && lastnode.getFeatureHolder().isHasNested()) {
                    List<AbstractAst> nodes = readArrayOfAst(stream, config);
                    for (AbstractAst n : nodes) {
                        lastnode.addNestedAst(n);
                    }
                } else {
                    if (stream.eof()) {
                        break;
                    }
                    int start = stream.getIndex();
                    readArrayOfAst(stream, config);
                    int end = stream.getIndex();
                    String message = String.format(config.getLocalization().getString(Localization.RULE_CANNOT_HAVE_NESTED_ITEMS));
                    throw new SelebashParseException(message, start, end);
                }
            } else {
                int startIndex = stream.getIndex();
                String word = stream.readWord();
                if (SelebashParser.wordEqual(word, config.getLocalization().getString(Localization.IF))) {
                    AbstractAst ifAst = parseIf(stream, config, startIndex);
                    seqNodes.add(ifAst);
                } else if (SelebashParser.wordEqual(word, config.getLocalization().getString(Localization.CALL))) {
                    AbstractAst ifAst = parseCall(stream, config, startIndex);
                    seqNodes.add(ifAst);
                } else {
                    stream.setIndex(startIndex);
                    String line = stream.readStringUntilEndOfLine();
                    int endIndex = stream.getIndex();
                    AbstractAst node = processLine(stream, line, startIndex, endIndex, config);
                    seqNodes.add(node);
                    lastnode = node;
                    stream.getTabLevel();
                }
            }

            if (stream.eof()) {
                break;
            }
        }

        return seqNodes;
    }

    private AbstractAst parseCall(ParserInputStream stream, Config config, int prevIndex) {
        CallFunctionAst ast = new CallFunctionAst();
        ast.setStart(prevIndex);
        ast.setEnd(stream.getIndex());
        ast.setDocument(context.getCurrentDocument());
        stream.skipBlankEnsureCurrentString();
        String functionName = stream.readStringUntilEndOfLine();
        if (functionName == null) {
            throw new SelebashParseException("You forget to write function name", prevIndex, stream.getIndex());
        }

        functionName = Utils.stripQuotes(functionName.trim());
        ast.setFunctionName(functionName);
        stream.skipBlank();
        return ast;
    }

    private AbstractAst parseIf(ParserInputStream stream, Config config, int prevIndex) {
        IfAst ifAst = new IfAst();
        ifAst.setStart(prevIndex);
        ifAst.setEnd(stream.getIndex());
        ifAst.setDocument(context.getCurrentDocument());
        stream.skipBlankEnsureCurrentString();
        int tabLevel = stream.getTabLevel();
        int conditionStart = stream.getIndex();
        String conditionString = stream.readStringUntilEndOfLine();
        AbstractAst conditionAst = processLine(stream, conditionString, conditionStart, stream.getIndex(), config);
        ifAst.addCondition(conditionAst);
        stream.skipBlank();
        if (stream.getTabLevel() <= tabLevel) {
            throw new SelebashParseException("You should write steps for if expression, or may be you forget to increase ident level", prevIndex, stream.getIndex());
        }

        List<AbstractAst> thenAst = readArrayOfAst(stream, config);
        ifAst.addNestedAsts(thenAst);
        stream.skipBlank();
        if (stream.getTabLevel() != tabLevel) {
            return ifAst;
        }

        int afterIfIndex = stream.getIndex();
        String word = stream.readWord();
        if (word == null) {
            stream.setIndex(afterIfIndex);
            return ifAst;
        }
        if (SelebashParser.wordEqual(word, config.getLocalization().getString(Localization.ELSE))) {
            stream.skipBlank();
            if (stream.eof()) {
                throw new SelebashParseException("You should provide steps after else", afterIfIndex, stream.getIndex());
            }
            List<AbstractAst> elseAst = readArrayOfAst(stream, config);
            ifAst.addElseAstNodes(elseAst);
            stream.skipBlank();
        } else {
            stream.setIndex(afterIfIndex);
        }

        return ifAst;
    }

    public SequenceAst readSequence(ParserInputStream stream, Config config) {
        SequenceAst sequence = new SequenceAst();
        int start = stream.getIndex();
        List<AbstractAst> nodes = readArrayOfAst(stream, config);
        for (AbstractAst n : nodes) {
            sequence.addNestedAst(n);
        }
        int end = stream.getIndex();
        sequence.setStart(start);
        sequence.setEnd(end);
        sequence.setDocument(context.getCurrentDocument());
        return sequence;
    }

    private AbstractAst processLine(ParserInputStream stream, String line, int startIndex, int endIndex, Config config) {
        AbstractAst findedAst = null;
        for (FeatureHolder fh : context.getFeatures()) {
            Matcher m = fh.getPattern().matcher(line);
            if (m.matches()) {
                if (findedAst == null) {
                    findedAst = createAst(stream, line, startIndex, endIndex, fh);
                }else{
                    String message="Ambiguous line "+stream.getCurrentLine()+" two rules can be matched:["+findedAst.getFeatureHolder()+"] and ["+fh.toString()+"]";
                    throw new SelebashParseException(message, startIndex, endIndex);
                }
            }
        }

        if (findedAst == null) {
            throwError(line, startIndex, endIndex, config);
            return null;
        } else {
            return findedAst;
        }
    }

    private String bestPatternMatch(String line) {
        int bestIndex = -1;
        int bestdist = 9999999;
        int index = -1;
        for (FeatureHolder fh : context.getFeatures()) {
            index++;
            int dist = StringUtils.getLevenshteinDistance(fh.getPattern().pattern(), line);
            if (dist < bestdist) {
                bestIndex = index;
                bestdist = dist;
            }
        }

        if (bestIndex == -1) {
            return null;
        }

        return context.getFeatures().get(bestIndex).getPattern().pattern();
    }

    private void throwError(String line, int startIndex, int endIndex, Config config) throws SelebashParseException {
        String bestmatch = bestPatternMatch(line);
        if (bestmatch == null) {
            throw new SelebashParseException(config.getLocalization().getString(Localization.CANNOT_MATCH_RULE), startIndex, endIndex);
        }

        String message = String.format(config.getLocalization().getString(Localization.CANNOT_MATCH_RULE_BEST_MATCH), bestmatch) + ".  \"" + line + "\"";
        throw new SelebashParseException(message, startIndex, endIndex);
    }

    private AbstractAst createAst(ParserInputStream stream, String line, int startIndex, int endIndex, FeatureHolder featureHolder) {
        FeatureAst featureAst = new FeatureAst();
        featureAst.setFeatureHolder(featureHolder);
        featureAst.setLine(line);
        featureAst.setStart(startIndex);
        featureAst.setDocument(context.getCurrentDocument());
        featureAst.setEnd(endIndex);
        return featureAst;
    }
}
